package com.spzx.common.redis.util;


import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;


//记得将工具类放入自动配置文件中
@Component
public class RedisLockUtilV1 {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    //设置静态属性的作用是  为了在工具类实体化的时候 加载 默认值为null
    static StringRedisTemplate STRING_REDIS_TEMPLATE;


    //被标注的方法 会在对象初始化后执行
    @PostConstruct
    public void init(){
        //讲容器初始化属性值后的stringRedisTemplate  赋值给静态属性STRING_REDIS_TEMPLATE;
        STRING_REDIS_TEMPLATE=stringRedisTemplate;

    }

    //设置获取锁的方法  获取成功 可以继续执行后续的业务 否则自旋递归
    public static void lock(String key,String uuid,int timeout){
        //1、 尝试获取锁
        // 键保证多线程的互斥关系，uuid验证锁是否属于当前线程，timeout超时 防止死锁
        //如果设置重入锁 可以使用hash结构
        /*
        *           key             uuid         count
        *           test:lock       随机生成       1    (count来计算是否是最后一层锁)
        * */
        //setIfAbsent 方法的作用是：如果指定的键（key）不存在，
        // 则将其值设置为给定的值，并返回 true；如果键已经存在，则不进行任何操作，
        // 并返回 false。这个方法非常适用于需要确保某个键只被设置一次的场景，比如分布式锁的实现。
        Boolean flag = STRING_REDIS_TEMPLATE.opsForValue().setIfAbsent(key, uuid, timeout, TimeUnit.SECONDS);

        //2、 通过flag来检测是否获取锁成功  失败就进行递归调用 自旋
        if (!flag){
            //获取锁失败 进行自旋（递归）
            //为了防止栈溢出 递归调用时可以加上时间间隔 否则很快就会溢出
            try {
                Thread.sleep(50+new Random()
                        .nextInt(20,100));
                lock(key,uuid,timeout);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }


        }else {
            //3、如果获取成功 使用看门狗线程 对锁进行续期 防止锁失效的问题（如果业务时长超过锁的有效期，业务还未结束锁就过期了 其他线程也可以获取锁执行业务）
            renewLock(key,uuid,timeout);

        }



    }


    //看门狗线程 ： 锁自动续期的方法 需要和使用锁的线程 在同一个服务中开启
    private static void renewLock(String key,String uuid , int timeout){
        //另开一个异步线程
        new Thread(()->{
            //给看门狗线程设置一个时间  降低触发频率 减轻压力 时间是 锁过期时间的三分之二
            try {
                TimeUnit.SECONDS.sleep(timeout*2/3);
                //判断锁是否是自己的  如果是 则续期 否则停止循环（停止续期）
                while(uuid.equals(STRING_REDIS_TEMPLATE.opsForValue().get(key))){
                    //给锁续期
                    STRING_REDIS_TEMPLATE.expire(key,timeout,TimeUnit.SECONDS);
                    //再给看门狗睡眠
                    TimeUnit.SECONDS.sleep(timeout*2/3);

                }




            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }


        }).start();

    }


    //释放锁的方法
    public static void releaseLock(String key,String uuid){

        //if判断锁是否是自己的 ARGV代表第一个参数 KEYS是第一个键 键是Key参数是uuid  通过key获取cache中的值是否等于uuid来判断是否是自己的锁
        //then 如果是 则进行删除key 释放锁
        //如果不是自己的锁 则返回0 不进行释放
        String delLockLua = " if ARGV[1]==redis.call('get',KEYS[1]) "+
                    " then redis.call('del','KEYS[1]')  return 1 "+
                    " else return 0 end ";


        // 第一个参数 封装lua脚本对象 构造器参数lua脚本 参数2 设置Lua脚本返回值类型
        Long delFlag = STRING_REDIS_TEMPLATE.execute(new DefaultRedisScript<>(delLockLua, Long.class), Arrays.asList(key), uuid);

        if (delFlag==1){
            System.out.println("释放锁成功");
        }else if (delFlag==2){
            System.out.println("释放重入锁");
        }else {
            System.out.println("释放锁失败");
        }
    }








}
